PLOTTING USING MATPLOTLIB

Week 5 Part 1 of PubH 6852

Author
Affiliation

Dr JH Klopper

The Department of Biostatistics and Bioinformatics

INTRODUCTION

Data visualization is one of the richest forms of communication. Clear plots, that are not overloaded with information, can quickly and effectively convey results.

There are many plotting packages available in Python. These include matplotlib, seaborn, bokeh, altair, plotly, and many more.

The matplotlib package is arguably the best known and the largest plotting package in the Python ecosystem. Many other Python packages such as pandas, and even other plotting packages such as seaborn, require matplotlib as a dependency. In this notebook, we touch on some of the useful aspects of using matplotlib. It is an enormous package and we will concentrate on topics pertaining to its use in science in general.

The matplotlib homepage lists many examples. Full documentation is available as a PDF file for download.

There are two main ways to generate plots using matplotlib. The pyplot interface is easy to use and generates plots with a few simple commands. The object oriented interface requires more code, but is more flexible and allows for more customization of plots.

PACKAGES USED IN THIS NOTEBOOK

Below, we import the packages that we will use in this notebook. View the code comments for a hint about each package.

# Symbolic Python allows for the use of mathematical symbols and symbolic computation
import sympy as sym
# Numerical Python allows for numerical calculations
import numpy as np
# The pyplot module in the matplotlib package
import matplotlib.pyplot as plt
# The gridspec module in the matplotlib package
import matplotlib.gridspec as gridspec

The following magic command is used when coding on high pixel density screens such as those on Apple notebook computers. It renders plots with more sharpness.

%config InlineBackend.figure_format = 'retina'

The style.available attribute from the pyplot module shows a list of all the available plot styles.

# Plotting styles available on this computer
plt.style.available
['Solarize_Light2',
 '_classic_test_patch',
 '_mpl-gallery',
 '_mpl-gallery-nogrid',
 'bmh',
 'classic',
 'dark_background',
 'fast',
 'fivethirtyeight',
 'ggplot',
 'grayscale',
 'petroff10',
 'seaborn-v0_8',
 'seaborn-v0_8-bright',
 'seaborn-v0_8-colorblind',
 'seaborn-v0_8-dark',
 'seaborn-v0_8-dark-palette',
 'seaborn-v0_8-darkgrid',
 'seaborn-v0_8-deep',
 'seaborn-v0_8-muted',
 'seaborn-v0_8-notebook',
 'seaborn-v0_8-paper',
 'seaborn-v0_8-pastel',
 'seaborn-v0_8-poster',
 'seaborn-v0_8-talk',
 'seaborn-v0_8-ticks',
 'seaborn-v0_8-white',
 'seaborn-v0_8-whitegrid',
 'tableau-colorblind10']

I will use the default plotting style.

Finally, we use the magic command below to have the plots rendered in the notebook. In some development environments, this is done automatically.

%matplotlib inline

QUICK PLOTS WITH THE PYPLOT INTERFACE

Below, we look at three commonly used plot types using the pyplot interface. Later, we will look at the object oriented interface.

A list of links to the long list of pyplot plots and settings is available here.

We start with the humble line plot and the scatter plot.

LINE AND SCATTER PLOTS

We generate points that serve as x-axis values for the expression \sin{\left( x \right)} with some random noise taken from the standard normal distribution for two function y_{1} \left( x \right) and y_{2} \left( x \right), shown in Equation 1.

\begin{align} &x = \left\{ 0, 0.256, 0.513, \ldots , 12.310 \right\} \nonumber \\ &y_{1} \left( x \right) = sin{\left( x \right)} + \frac{N \left[ \mu = 0 , \sigma = 1 \right]}{10} \nonumber \\ &y_{2} \left( x \right) = sin{\left( x \right)} + \frac{N \left[ \mu = 0 , \sigma = 1 \right]}{10} \end{align} \tag{1}

Since we are using the randn function from the numpy random module, we will get different results each time we run the code cell below.

xvals = np.linspace(0, 4 * np.pi, 50) # Start, stop, number of points
yvals_1 = np.sin(xvals) + np.random.randn(len(xvals)) * 0.1 # Experiment 1
yvals_2 = np.sin(xvals) + np.random.randn(len(xvals)) * 0.1 # Experiment 2

We start with a bare bones line plot in Figure 1 using the pyplot plot function. Note that the arguments are simple the x and y-axis values..

plt.plot(xvals, yvals_1); # Semicolon suppresses other output to the screen
Figure 1: Simple line plot

We can add a variety of arguments to the plot function. Below we set values for a few often used arguments. These are explained in the code comments.

marker_type = 'o--' # Dashes with dots at actual values
color_1 = 'deepskyblue' # Color of first function
color_2 = 'orange' # Color of second function
lw = 1 # Line width
ms = 5 # Marker size
label_1 = 'Experiment 1' # Label for values
label_2 = 'Experiment 2' # lable for values

The plot function creates a plot based on argument values. Values for the figure size, title, and axes labels are added. The marker type shows dots with interpolated (straight) lines between (shown as dashed lines). We view this plot in Figure 2.

plt.plot(xvals, yvals_1, marker_type, color=color_1, lw=lw, ms=ms, label=label_1)
plt.plot(xvals, yvals_2, marker_type, color=color_2, lw=lw, ms=ms, label=label_2)
Figure 2: Line and marker plot for two data sets

Now we add many more arguments with the assigned values. We start with the figure function and add the figsize argument with its values represented as a tuple of values for the width and height.

The legend function adds a legend, which we place in the upper right corner and set the font size. We add x and y axes labels (see comment below plot), add a grid, and finally change parameters of the axes tick marks.

plt.figure(figsize=(8, 4)) # Size of the plot
plt.plot(xvals, yvals_1, marker_type, color=color_1, lw=lw, ms=ms, label=label_1)
plt.plot(xvals, yvals_2, marker_type, color=color_2, lw=lw, ms=ms, label=label_2)
plt.legend(loc='upper right', fontsize=8) # Legend with position and font size
plt.xlabel(r'Time $\left[ \mu s \right]$') # x axis title with TeX
plt.ylabel(r'Current $\left[ A \right]$')# y axis title
plt.title('Current over time') # Plot title
plt.grid() # Add x and y grid lines
plt.tick_params(which='both', direction='in'); # Ticks face into plot area

You will note the addition of Tex in the x-axis label. It is added as raw text (the r prefix). TeX is enclosed in a pair of $ symbols. A full list of the subset of TeX markup that can be used in matplotlib is available here.

Below, we add a model, \sin{\left( x \right)}, indicated by more points and a solid line. We also remove the dashed lines of the data interpolation.

xvals_model = np.linspace(0, 4 * np.pi, 200)
yvals_model = np.sin(xvals_model)

The plot is redone with added features. The ylim function allows a bit of breathing room for the legend.

plt.figure(figsize=(8, 4)) # Size of the plot
plt.plot(xvals, yvals_1, 'o', color=color_1, ms=ms, label=label_1)
plt.plot(xvals, yvals_2, 'o', color=color_2, ms=ms, label=label_2)
plt.plot(xvals_model, yvals_model, '-', label='Model')
plt.legend(loc='upper right', fontsize=8, ncol=3) # Legend with position and font size and columns
plt.xlabel(r'Time $\left[ \mu s \right]$') # x axis title with TeX
plt.ylabel(r'Current $\left[ A \right]$')# y axis title
plt.ylim(top=1.5) # Adding space for the legend
plt.title('Current over time and model') # Plot title
plt.grid()
plt.tick_params(which='both', direction='in');

Without the interpolated lines, these plots are actually scatter plots. Below, we generate data for an independent and a dependent variable.

indep = np.random.randint(100, 300, 50) / 2
dep = indep * 0.8 + np.random.randint(-10, 10, 50)

The 'o' argument plots markers only.

plt.figure(figsize=(6, 6))
plt.plot(indep, dep, 'o')
plt.title('Scatter plot')
plt.xlabel('Independent variable')
plt.ylabel('Dependent variable')
plt.grid()
plt.tick_params(which='both', direction='in');

The scatter function also generates scatter plots. We can visualize another numerical variable by setting the size, s, of the markers. A fourth numerical variable can be included by coloring, c, the markers.

indep2 = np.random.randint(50, 200, 50)
indep3 = np.random.randint(1000, 2000, 50)
plt.figure(figsize=(6, 6))
plt.scatter(indep, dep, s=indep2, c=indep3, alpha=0.5)
plt.title('Scatter plot')
plt.xlabel('Independent variable')
plt.ylabel('Dependent variable')
plt.grid()
plt.tick_params(which='both', direction='in');

Both the x and y-axes are linear. We can change this to log scales. We start by generating values for plots and then plot with the y axis scale set to \log_{10}.

vals_for_log = [10**i for i in range(10)]

plt.plot(vals_for_log, 'o--')
plt.yscale('log')
plt.title('Log scale for $y$ axis')
plt.ylabel(r'$\log_{10}$ scale')
plt.grid()
plt.tick_params(which='both', direction='in');

We do the same for the x axis.

plt.plot(vals_for_log, range(10), 'o--')
plt.xscale('log')
plt.title('Log scale for $x$ axis')
plt.xlabel(r'$\log_{10}$ scale')
plt.grid()
plt.tick_params(which='both', direction='in');

The axes can use different log scales.

vals_for_x = [10**i for i in range(1, 6)]
vals_for_y = [2**i for i in range(1, 6)]
plt.plot(vals_for_x, vals_for_y, 'o--')
plt.semilogx()
plt.semilogy(base=2)
plt.title('Log base 10 and log base 2')
plt.xlabel(r'$\log_{10}$ scale')
plt.ylabel(r'$\log_{2}$ scale')
plt.grid()
plt.tick_params(which='both', direction='in');

If both axes share the same log scale, we can use the loglog function.

vals_for_x = [10**i for i in range(1, 6)]
vals_for_y = [10**i for i in range(6, 11)]
plt.plot(vals_for_x, vals_for_y, 'o--')
plt.loglog(base=10)
plt.title('Log base 10 on both axes')
plt.xlabel(r'$\log_{10}$ scale')
plt.ylabel(r'$\log_{10}$ scale')
plt.grid()
plt.tick_params(which='both', direction='in');

FREQUENCY PLOTS

Frequency plots indicate the count of classes for a categorical variable or binned numerical variables (intervals). The former is called a bar plot and the latter a histogram. In a relative frequency plot, we divide by the sample size.

Below, we generate random variables for two groups taken from normal distributions using the normal function in the random module of the numpy package. The first array, var_group_1, holds values from a normal distribution with a mean of 100 and a standard deviation of 10. The second, var_group_2 holds values from a normal distribution with a mean of 98 and a standard deviation of 15. Both numpy arrays hold 1000 random values.

var_group_1 = np.random.normal(loc=100, scale=10, size=1000)
var_group_2 = np.random.normal(loc=98, scale=15, size=1000)

We start with a bare bones histogram. We use the hist function and pass one of the arrays of random values as argument. The matplotlib package will divide the numerical variable into equally sized intervals and the height of each bar in the histogram is an indication of the number of values in the array that will in each interval.

plt.hist(var_group_1)
(array([  3.,  28.,  58., 169., 232., 251., 147.,  84.,  24.,   4.]),
 array([ 67.44086786,  73.80963403,  80.1784002 ,  86.54716637,
         92.91593253,  99.2846987 , 105.65346487, 112.02223104,
        118.39099721, 124.75976337, 131.12852954]),
 <BarContainer object of 10 artists>)

Two arrays and a plot are returned. The first array indicates the frequency (count) of values in each bin (interval). The second array indicates the intervals created by matplotlib. We can overwrite the intervals. Below, we create eight intervals (requiring nine values) using list comprehension.

bin_intv = [10 * i for i in range(6, 15)]
bin_intv
[60, 70, 80, 90, 100, 110, 120, 130, 140]

Bins are right-open intervals, except for the last which is a closed interval. Our bin intervals above are therefor \left\{ [60,70) , [70,80) , \ldots , [120,130), [130,140] \right\}.

The bins argument can now be set to these intervals.

plt.hist(var_group_1, bins=bin_intv, color='gray')
(array([  2.,  28., 134., 357., 328., 131.,  19.,   1.]),
 array([ 60.,  70.,  80.,  90., 100., 110., 120., 130., 140.]),
 <BarContainer object of 8 artists>)

We can also simply specify the number of bins, though. Below, we create a histogram for both groups. The first sets the number of bins to 10 and the second specifies the intervals. The histogram type, histtype, is set to step. This shows the outlines of the bars only. We also add a legend, specifying a two column setup.

plt.figure(figsize=(10, 5))
plt.hist(var_group_1, bins=10, histtype='step', lw=2, label='Group 1')
plt.hist(var_group_2, bins=bin_intv, histtype='step', lw=2, label='Group 2')
plt.legend(loc='upper right', fontsize=8, ncol=2)
plt.title('Frequency plot')
plt.xlabel('Variable value')
plt.ylabel('Frequency')
plt.grid()
plt.tick_params(which='both', direction='in');

The classes of a categorical variable can also be visualized as a frequency chart. In this case, we use a bar chart. There are spaces between the bars to indicate that the variable is not continuous numerical as in a histogram with no spaces between the bars.

Below, we create a Python list object with five elements.

classes = ['A', 'B', 'C', 'D']

Now, we generate two computer variables each assigned an array with 500 elements taken from the list above (with replacement). This is achieved using the choice function.

cat_group_1 = np.random.choice(classes, 500)
cat_group_2 = np.random.choice(classes, 500)

The numpy unique function, with argument return_counts set to True returns an array with all the classes and an array with the frequency of each class.

np.unique(cat_group_1, return_counts=True)
(array(['A', 'B', 'C', 'D'], dtype='<U1'), array([125, 125, 127, 123]))

Below, we assign the frequencies to two individual arrays. Note the use of indexing as the unique function (with return_counts argument) returns a tuple with two elements (each being an array) and we are interested in the second element of the tuple.

cnts_1 = np.unique(cat_group_1, return_counts=True)[1]
cnts_2 = np.unique(cat_group_2, return_counts=True)[1]

We generate a simple bar chart using the bar function.

plt.figure(figsize=(10, 5))
plt.grid(axis='y')
plt.bar(classes, cnts_1, color='darkgrey')
plt.title('Bar plot')
plt.xlabel('Classes'),
plt.ylabel('Frequency');

It is more difficult to produce a grouped bar plot. We have to subtract and add to the x axis values using a numpy array, and then set the width to generate grouped bar plots. Below, we subtract 0.2 and add 0.2 and set a width of 0.4. The four numerical x axis values are then changed using the xticks function.

plt.figure(figsize=(10, 5))
plt.grid(axis='y')
plt.bar(np.arange(4)-0.2, cnts_1, 0.4, label='Group 1', color='gray')
plt.bar(np.arange(4)+0.2, cnts_2, 0.4, label='Group 2', color='lightgray')
plt.xticks(range(4), classes) # Replace 1, 2, 3, 4 with A, B, C, D
plt.legend(loc='upper right', fontsize=8, ncol=2)
plt.ylim(top=160); # Adding a margin for the legend

BAR PLOTS FOR STATISTICS

Bar plots can also be used to indicate statistics such as mean and standard deviation. Below, we generate some data for the coefficient of thermal expansion of two metals. The height of the bars indicate the means and the error bars visualise the standard deviation.

aluminium = np.random.normal(loc=0.00004, scale=0.000015, size=100)
copper = np.random.normal(loc=0.000025, scale=0.00001, size=100)
al_mean = np.mean(aluminium)
al_std = np.std(aluminium, ddof=1)

cu_mean = np.mean(copper)
cu_std = np.std(copper, ddof=1)
plt.figure(figsize=(5, 5))
plt.bar(range(2),
        [al_mean, cu_mean],
        0.4,
        yerr=[al_std, cu_std],
        align='center',
        capsize=8,
        color='black',
        alpha=0.1)
plt.grid(axis='y')
plt.title('Coefficient of thermal expansion')
plt.xticks(range(2), ['Aluminium', 'Copper'])
plt.ylabel('Coefficient of thermal expansion $[{}^{o}C^{-1}]$');

BOX AND WHISKER PLOTS

Box-and-whisker plots also give an indication of the distribution of values for a numerical variable. Below, we use the boxplot function to visualise the same information as in the histograms above.

plt.figure(figsize=(10, 5))
plt.boxplot([var_group_1, var_group_2],
            flierprops={'marker':'D'},
            meanline=True,
            showmeans=True, # Show mean
            notch=True, # To indicate CI
            bootstrap=5000) # 95% CI around the median
plt.title('Box-and-whisker plot for variable values in two groups')
plt.xlabel('Group')
plt.ylabel('Variable value')
plt.xticks([1, 2],['Group 1', 'Group 2']) # Change 1 and 2 to Group 1 and Group 2
plt.tick_params(which='both', direction='in')
plt.grid(axis='y');

OBJECT ORIENTED INTERFACE

More options become available when we use the object oriented matplotlib interface. You can read more about subplots here.

fig, ax = plt.subplots() # Single plot

fig, ax = plt.subplots(3, 2, figsize=(16, 9)) # Three rows and two columns of plots

Below, we create a single row of plots, with two columns. We also add a text box to the histogram.

txt = '\n'.join((r'$\sigma_{1}=%.2f$'%(np.std(var_group_1)), r'$\sigma_{2}=%.2f$'%(np.std(var_group_2))))
txt
'$\\sigma_{1}=9.93$\n$\\sigma_{2}=15.65$'
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6), frameon=False)

ax1.hist(var_group_1, bins=bin_intv, lw=2, histtype='step', label='Group 1')
ax1.hist(var_group_2, bins=bin_intv, lw=2, histtype='step', label='Group 2')
ax1.legend(loc='upper right', fontsize=8, ncol=2, edgecolor='gray')
ax1.set_title('Frequency plot')
ax1.set_xlabel('Variable value')
ax1.set_ylabel('Frequency')
ax1.text(60, 200, txt, bbox={'facecolor':'white', 'edgecolor':'gray'})
ax1.tick_params(which='both', direction='in')
ax1.grid()

ax2.boxplot([var_group_1, var_group_2], flierprops={'marker':'D'})
ax2.set_title('Box-and-whisker plot for variable values in two groups')
ax2.set_xlabel('Group')
ax2.set_ylabel('Variable value')
ax2.yaxis.grid(True)
ax2.set_xticklabels(['Group 1', 'Group 2'])
ax2.tick_params(which='both', direction='in')

fig.text(0.5, -0.05, 'Experiment 1', ha='center', fontsize=12);

In line 9 above where we have ax1.text we set the x and y coordinates according to the axes tick values of the plot. If we use the keyword argument and value transform=ax1.transAxes the coordinates will be detached from the tick values. Now 0,0 will be the left bottom corner and 1,1 will be the top right corner.

The gridspec module allows even more flexibility when it comes to plots. Below, we incorporate three of our previous plots in two rows. The top row spans a single plot and the bottom row has plots in two columns.

fig = plt.figure(tight_layout=True, figsize=(16, 9))
gs = gridspec.GridSpec(2, 2)

ax1 = fig.add_subplot(gs[0, :])
ax1.plot(xvals, yvals_1)
ax1.grid(True)
ax1.set_title('Basic plot')

ax2 = fig.add_subplot(gs[1, 0])
ax2.plot(xvals, yvals_1, marker_type, color=color_1, lw=lw, ms=ms, label=label_1)
ax2.plot(xvals, yvals_2, marker_type, color=color_2, lw=lw, ms=ms, label=label_2)
ax2.legend(loc='upper right', fontsize=8)
ax2.set_xlabel(r'Time $\left[ \mu s \right]$')
ax2.set_ylabel('Current [A]')
ax2.set_title('Current over time')
ax2.tick_params(which='both', direction='in')
ax2.grid(True)

ax3 = fig.add_subplot(gs[1, 1])
ax3.plot(xvals, yvals_1, 'o', color=color_1, ms=ms, label=label_1)
ax3.plot(xvals, yvals_2, 'o', color=color_2, ms=ms, label=label_2)
ax3.plot(xvals_model, yvals_model, '-', label='Model')
ax3.legend(loc='upper right', fontsize=8, ncol=3)
ax3.set_xlabel(r'Time $\left[ \mu s \right]$')
ax3.set_ylim(top=1.5)
ax3.set_title('Current over time and model')
ax3.tick_params(which='both', direction='in')
ax3.grid(True);

PLOTS FOR FUNCTIONS AND VECTORS

CONTOUR PLOTS

There are two types of contour plots. The contourf function creates filled contour plots and the contour function creates contour lines only (without the colour fill).

Below, we consider the function f \left( x,y \right) = x^{2} + y^{2}.

We have to use numpy to set up a grid of x and y coordinates. The linspace function can generate an array of values that we can use for both axes. We calculate coordinate values for each point on the grid and assign the function to it.

_ = np.linspace(-1, 1, 200)
X, Y = np.meshgrid(_, _)
f = X**2 + Y**2

Now, we create a filled contour plot.

plt.figure(figsize=(8, 6))
plt.axis('equal')
plt.xlim(-1, 1)
plt.contourf(X, Y, f, levels=30, cmap='inferno')
plt.colorbar(label='Value of $f(x,y)$')
plt.title('Filled contour plot');
Ignoring fixed x limits to fulfill fixed data aspect with adjustable data limits.

We can add the vmin or the vmax arguments to the contourf function. Either of these can be used to set the limit beyond which the colour is constant. This is helpful for infinities.

Instead of a filled contour plot, we an view only the contour lines. Below, we add values to the contours themselves instead of adding a color bar.

fig = plt.contour(X, Y, f, levels=10, cmap='plasma')
plt.clabel(fig, fontsize=8)
plt.axis('equal')
plt.title('Contour plot with values');

QUIVER PLOTS

Quiver plots generate vectors. A vector takes an initial x and y coordinate and then a magnitude for each of the two coordinate directions.

# Coordinate positions for tail of vector
x_pos = 0
y_pos = 0

# Magnitude for x and y directions (not to scale)
x_direct = 2
y_direct = 1

We now plot the vector and with the values above and set the x axis and y axis limits.

fig, ax = plt.subplots(figsize=(3, 3))
ax.quiver(x_pos, y_pos, x_direct, y_direct, scale=10) # Scale the magnitude
ax.axis([-0.01,0.03, -0.01, 0.03]) # Axes limits
ax.set_aspect('equal'); # Set equal aspect ratio for the axes

We do not create individual vectors for a vector field. Below we see a function, \mathbf{F}, representing a vector field.

\begin{align} &\mathbf{F} = \frac{x}{5} \hat{i} - \frac{y}{5} \hat{j} \notag \\ &\mathbf{F}\left( x,y \right) = \left( \frac{x}{5}, -\frac{y}{5} \right) \notag \end{align}

We use the meshgrid function again. The variables u and v hold the components of \mathbf{F}.

_ = np.arange(0, 2.2, 0.2) # Starting points
X, Y = np.meshgrid(_, _) # Mesh of x and y coordinates
u = X/5 # x direction
v = -Y/5 # y direction

The quiver function creates multiple vectors demonstrating the vector field.

fig, ax = plt.subplots(figsize=(7,7))
ax.quiver(X, Y, u, v)
ax.axis([-0.2, 2.3, -0.2, 2.1])
ax.set_aspect('equal')
ax.set_title('Vector field $\mathbf{F}$');
<>:5: SyntaxWarning:

invalid escape sequence '\m'

<>:5: SyntaxWarning:

invalid escape sequence '\m'

/var/folders/b4/vzdym89j18q7frz99dqhdkjw0000gp/T/ipykernel_9117/4252099361.py:5: SyntaxWarning:

invalid escape sequence '\m'

We can also use the quiver function for a gradient field. We start with a multivariable function shown below.

f \left( x,y \right) = x^{2} - y^{2}

We are interested in the gradient.

\nabla f = \left( \frac{\partial{f}}{\partial{x}} , \frac{\partial{f}}{\partial{y}} \right) = \left( f_{x} , f_{y} \right)

Sympy can be used to calculate the partial derivatives analytically. We set the variables x and y to be mathematical symbols.

x, y = sym.symbols('x y')

The variable f holds our symbolic function.

f = x**2 - y**2
f

\displaystyle x^{2} - y^{2}

We consider the partial derivative of f with respect to x, written as f_{x} using the diff method.

f.diff(x) # First derivative of f with respect to x

\displaystyle 2 x

The partial derivative of f with respect to y is written as f_{y}.

f.diff(y) # First derivative of f with respect to x

\displaystyle - 2 y

We consider the point p \left( 2,2 \right). Below, we calculate f_{x} \left( p \right). The subs method allows us to substitute values for our variables.

f.diff(x).subs(x, 2).subs(y, 2)

\displaystyle 4

We also calculate f_{y} \left( p \right).

f.diff(y).subs(x, 2).subs(y, 2)

\displaystyle -4

We now have that \nabla f \left( p \right) = \left( 4,-4 \right). We can view this point on our gradient plot to make sure that our plot is accurate.

_ = np.linspace(-2, 2, 20)
X, Y = np.meshgrid(_, _)
u = 2 * X # Partial drivative of f with respect to x
v = -2 * Y # Partial drivative of f with respect to y

We now create the gradient plot.

fig, ax = plt.subplots(figsize=(7, 7))
ax.quiver(X, Y, u, v)
ax.set_aspect('equal')
ax.set_title('Gradient plot')
ax.set_xlabel('$x$ axis')
ax.set_ylabel('$y$ axis');

The point p does indeed show a slope of \left( 4, -4 \right).

STREAM PLOTS

Stream plots can likewise help us visualise a vector field. Below, we use sub plots to create three plots. The first is a basic stream plot of our previous function f. The magnitude of the vector at each coordinate point is demonstrated using colour in the second plot. The last plot uses variable line width to demonstrate the magnitude of the vector as each coordinate. We calculate the magnitude in the usual way using \sqrt{x^{2} + y^{2}}.

fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True) # Set single y axis label

ax = axes[0]
ax.streamplot(X, Y, u, v)
ax.set_title('Basic stream plot')

ax = axes[1]
speed = np.sqrt(u**2 + v**2) # Magnitude of each vector
ax.streamplot(X, Y, u, v, color=speed, cmap='magma')
ax.set_title('Colour stream plot')

ax = axes[2]
lw = 4 * speed / np.max(speed)
ax.streamplot(X, Y, u, v, linewidth=lw)
ax.set_title('Velocity stream plot');